const float STAR_RADIUS               = 15.0f;
const float MINI_STAR_RADIUS          = 6.0f;
const int   MAXIMUM_STARS             = 30;
const int   MAXIMUM_MINI_STAR_GROUPS  = 6;
const int   MAXIMUM_BAD_STARS         = 10; // arbitrary

enum colour {
  RED,
  YELLOW
};

class Star {

protected:
  vec4 position_;
  vec4 velocity_;
  vec4 acceleration_;

  float spinSpeed_;
  float angle_;

  bool waiting_;
  bool collected_;

public:

  Star() {
    velocity_ = vec4(0, 0, 0, 0);
    acceleration_ = vec4(0, 0, 0, 0);
    angle_ = 0;
    waiting_ = true;
    collected_ = false;
  }

  void setPosition(vec4 p)       {position_     = p;}
  void setAngle(float a)         {angle_        = a;}
  void setSpinSpeed(float speed) {spinSpeed_    = speed;}
  void setVelocity(vec4 v)       {velocity_     = v;}
  void setAcceleration(vec4 a)   {acceleration_ = a;}

  void update() {
    angle_ += spinSpeed_;

    if (angle_ > TWO_PI) {
      angle_ -= TWO_PI;
    }

    velocity_ = velocity_ + acceleration_;
    position_ = position_ + velocity_;
    
  }

  /*
  void draw(shader &shader, texture* tex, mat4 &modelToProjection) {    
    glEnable(GL_BLEND);

    // set the texure into slot 0
    tex[collected_ ? TX_GREEN_STAR : TX_YELLOW_STAR].render(0);

    // set the uniforms
    shader.render(modelToProjection);

    float cosAngleDistance = (STAR_RADIUS * ROOT_2 * cos(PI_BY_4 - angle_));
    float sinAngleDistance = (STAR_RADIUS * ROOT_2 * sin(PI_BY_4 - angle_));

    // set the attributes    
    float vertices[4*4] = {
      position_[0] - cosAngleDistance, position_[1] - sinAngleDistance, 0, 0,
      position_[0] + sinAngleDistance, position_[1] - cosAngleDistance, 1, 0,
      position_[0] + cosAngleDistance, position_[1] + sinAngleDistance, 1, 1,
      position_[0] - sinAngleDistance, position_[1] + cosAngleDistance, 0, 1,
    };

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(2);

    // kick the draw
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisable(GL_BLEND);
  }
  */

  bool isWaiting() {return waiting_;}
  bool isCollected() {return collected_;}

  void wait() {
    waiting_ = true;
    collected_ = false;  
  }

  void collect() {collected_ = true;}

  void drop() {waiting_ = false;}

  vec4 getPosition() {return position_;}
  float getX() {return position_[0];}
  float getY() {return position_[1];}
  float getAngle() {return angle_;}

  ~Star() {}
};

class BadStar : public Star {

  
public:
  /*
  void draw(shader &shader, texture* tex, mat4 &modelToProjection) {
    glEnable(GL_BLEND);

    // set the texure into slot 0
    
    tex[TX_RED_STAR].render(0);

    // set the uniforms
    shader.render(modelToProjection);

    float cosAngleDistance = (STAR_RADIUS * ROOT_2 * cos(PI_BY_4 - angle_));
    float sinAngleDistance = (STAR_RADIUS * ROOT_2 * sin(PI_BY_4 - angle_));

    // set the attributes    
    float vertices[4*4] = {
      position_[0] - cosAngleDistance, position_[1] - sinAngleDistance, 0, 0,
      position_[0] + sinAngleDistance, position_[1] - cosAngleDistance, 1, 0,
      position_[0] + cosAngleDistance, position_[1] + sinAngleDistance, 1, 1,
      position_[0] - sinAngleDistance, position_[1] + cosAngleDistance, 0, 1,
    };

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(2);

    // kick the draw
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisable(GL_BLEND);
  }
  */

  //bool isWaiting() {return waiting_;}
  //void drop() {waiting_ = false;}
  //void setPosition(vec4 p)       {position_     = p;}
  //void setSpinSpeed(float speed) {spinSpeed_    = speed;}
  //void setVelocity(vec4 v)       {velocity_     = v;}
  //void wait() {waiting_ = true;}

  /*void update() {
    angle_ += spinSpeed_;

    if (angle_ > TWO_PI) {
      angle_ -= TWO_PI;
    }

    velocity_ = velocity_ + acceleration_;
    position_ = position_ + velocity_;
    
  }
  
  vec4 getPosition() {return position_;}
  float getX() {return position_[0];}
  float getY() {return position_[1];}
  float getAngle() {return angle_;}
  */
};

class MiniStarGroup {

  vec4 centre_;
  float displacement_;
  float speed_;
  float deceleration_;

  int colour_;

  bool waiting_;

public:
  MiniStarGroup() {
    reset();
  }

  void reset() {
    centre_ = vec4(0, 0, 0, 1);
    displacement_ = 0.0f;
    speed_ = 0.0f;
    waiting_ = true;
    deceleration_ = 0.3f;
  }

  bool isWaiting() {return waiting_;}

  void wait() {
    waiting_ = true;
  }

  void startDisplay(float x, float y) {
    waiting_ = false;
    centre_ = vec4(x, y, 0, 1);
    speed_ = 4.2f;
    displacement_ = 0.0f;
  }

  void update() {
    // Each mini star loses speed in their respective direction

    // Decrease speed. Update velocities accordingly
    speed_ -= deceleration_;
    displacement_ += speed_;
  }

  void draw(Sprite &sprite, shader &shader, texture* tex, mat4 &modelToProjection) {
    sprite.draw(vec4(centre_[0] - displacement_, centre_[1] + displacement_, 0, 1), true, shader, tex, modelToProjection);
    sprite.draw(vec4(centre_[0] + displacement_, centre_[1] + displacement_, 0, 1), true, shader, tex, modelToProjection);
    sprite.draw(vec4(centre_[0] - displacement_, centre_[1] - displacement_, 0, 1), true, shader, tex, modelToProjection);
    sprite.draw(vec4(centre_[0] + displacement_, centre_[1] - displacement_, 0, 1), true, shader, tex, modelToProjection);
  }
  
  void setCentre(vec4 c)     {centre_ = c;}
  void setSpeed(float s)     {speed_ = s;}
  void setColour(int colour) {colour_ = colour;}

  float getSpeed() {return speed_;}
};

class StarManager {

  Star stars[MAXIMUM_STARS];
  MiniStarGroup miniStars_[MAXIMUM_MINI_STAR_GROUPS];
  BadStar badStars_[MAXIMUM_BAD_STARS];

  Sprite yellowStarSprite_;
  Sprite redStarSprite_;
  Sprite greenStarSprite_;
  Sprite miniStarSprite_;

  int leftBound_;
  int topBound_;
  int rightBound_;
  int bottomBound_;

  int numberOfFallingStars;
  int numberOfMiniStarGroups;

  int numberOfBadStars_;
  int maximumNumberOfBadStars_; // increases with difficulty

  unsigned int score_;
  unsigned int lives_;

  StarManager() {
    reset();
    yellowStarSprite_ = Sprite(TX_YELLOW_STAR, vec4(STAR_RADIUS, STAR_RADIUS, 0, 0));
    greenStarSprite_ = Sprite(TX_GREEN_STAR, vec4(STAR_RADIUS, STAR_RADIUS, 0, 0));
    redStarSprite_ = Sprite(TX_RED_STAR, vec4(STAR_RADIUS, STAR_RADIUS, 0, 0));
    miniStarSprite_ = Sprite(TX_YELLOW_STAR, vec4(MINI_STAR_RADIUS, MINI_STAR_RADIUS, 0, 0));
  }

public:
  static StarManager &get()
  {
    static StarManager singleton;
    return singleton;
  }

  void reset() {
    score_ = 0;
    lives_ = 3;
    numberOfFallingStars = 0;
    numberOfMiniStarGroups = 0;
    numberOfBadStars_ = 0;
    maximumNumberOfBadStars_ = 1;

    for (int star = 0; star != MAXIMUM_STARS; ++star) {
      unsigned long seconds = (unsigned long)time(NULL);
      float randomSpinSpeed = Mother(&seconds) * 0.2f + 0.15f;

      stars[star].setSpinSpeed(randomSpinSpeed);
      stars[star].setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
      stars[star].wait();
    }

    for (int star = 0; star != MAXIMUM_BAD_STARS; ++star) {
      unsigned long seconds = (unsigned long)time(NULL);
      float randomSpinSpeed = Mother(&seconds) * 0.2f + 0.15f;

      badStars_[star].setSpinSpeed(randomSpinSpeed);
      badStars_[star].setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
      badStars_[star].wait();
    }

    for (int i = 0; i != MAXIMUM_MINI_STAR_GROUPS; ++i) {
      miniStars_[i].reset();
    }
  }

  void init(int left, int top, int right, int bottom) {
    leftBound_   = left;
    topBound_    = top;
    rightBound_  = right;
    bottomBound_ = bottom;

    for (int star = 0; star != MAXIMUM_STARS; ++star) {
      unsigned long seconds = (unsigned long)time(NULL);
      float randomSpinSpeed = Mother(&seconds) * 0.2f + 0.15f;

      stars[star].setSpinSpeed(randomSpinSpeed);
      stars[star].setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
    }

    for (int star = 0; star != MAXIMUM_BAD_STARS; ++star) {
      unsigned long seconds = (unsigned long)time(NULL);
      float randomSpinSpeed = Mother(&seconds) * 0.2f + 0.15f;

      badStars_[star].setSpinSpeed(randomSpinSpeed);
      badStars_[star].setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
    }
  }

  void starfall(Star &star, int distanceTravelled) {
    unsigned long seconds = (unsigned long)time(NULL);
    float randomXPosition = Mother(&seconds) * 200 - 100;

    star.drop();
    star.setPosition(vec4(randomXPosition, topBound_ + STAR_RADIUS, 0, 1));
    star.setVelocity(vec4(0.0f, -1.0f - ((float)distanceTravelled / 10000.0f), 0.0f, 0.0f));
    ++numberOfFallingStars;
  }

  void starfall(BadStar &star, int distanceTravelled) {
    unsigned long seconds = (unsigned long)time(NULL);
    float randomXPosition = Mother(&seconds) * 200 - 100;

    star.drop();
    star.setPosition(vec4(randomXPosition, topBound_ + STAR_RADIUS, 0, 1));
    star.setVelocity(vec4(0.0f, -1.0f - ((float)distanceTravelled / 10000.0f), 0.0f, 0.0f));
    ++numberOfBadStars_;
  }

  void resetStar(Star &star) {
    star.setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
    star.wait();
    star.setAcceleration(vec4(0, 0, 0, 0));
    star.setVelocity(vec4(0, 0, 0, 0));
    --numberOfFallingStars;
  }

  void resetStar(BadStar &star) {
    star.setPosition(vec4(0, topBound_ + STAR_RADIUS, 0, 1));
    star.wait();
    star.setVelocity(vec4(0, 0, 0, 0));
    --numberOfBadStars_;
  }

  void update(Balloon *balloon, int distanceTravelled, bool soundOn) {
    float random;

    maximumNumberOfBadStars_ = (distanceTravelled / 1000) + 1;
    if (maximumNumberOfBadStars_ > MAXIMUM_BAD_STARS) {
      maximumNumberOfBadStars_ = MAXIMUM_BAD_STARS;
    }

    // random chance of falling star. (always at least 1 falling)
    if (numberOfFallingStars == 0) starfall(stars[0], distanceTravelled);
    else {
      unsigned long seconds = (unsigned long)time(NULL);
      random = Mother(&seconds) * 100;

      float chanceThreshold = 0.9f + 5 * ((float)distanceTravelled / 10000.0f);

      if (random <= chanceThreshold) {
        // if in chance, find a waiting star and set it off.
        for(int star = 0; star != MAXIMUM_STARS; ++star) {
          if (stars[star].isWaiting()) {
            starfall(stars[star], distanceTravelled);
            break;
          }
        } // end for
      }
    }

    // update all stars that aren't waiting
    for(int star = 0; star != MAXIMUM_STARS; ++star) {

      if (!stars[star].isWaiting()) {

        stars[star].update();

        if (stars[star].isCollected()) {
          vec4 diff = vec4( (leftBound_ - 10) - stars[star].getX(), (topBound_ - 10) - stars[star].getY(), 0, 0);
          vec4 acceleration = diff * (70.0f / diff.lengthSquared());
          stars[star].setAcceleration(acceleration);

          if (stars[star].getX() > (rightBound_ - 30) && stars[star].getY() > (topBound_ - 30)) {
            resetStar(stars[star]);
            ++score_;
          }
        }
        
        else {
          // collision detection
          vec4 difference = vec4(stars[star].getX() - balloon->getX(), stars[star].getY() - balloon->getY(), 0, 0);
          float proximitySquared = difference.lengthSquared();

          if (proximitySquared < ((BALLOON_RADIUS + STAR_RADIUS - 4) * (BALLOON_RADIUS + STAR_RADIUS - 4))) {
            stars[star].collect();
            if (soundOn) {
            char name[2];
              name[0] = 1 + '0';
              name[1] = 0;
              sound_manager::play(vec4(0, 0, 0, 0), name);
            }

            for (int i = 0; i != MAXIMUM_MINI_STAR_GROUPS; ++i) {
              if (miniStars_[i].isWaiting()) {
                miniStars_[i].startDisplay(stars[star].getX(), stars[star].getY());
                ++numberOfMiniStarGroups;
                break;
              }
            }
          }

          // tell any stars out of bounds to wait.
          else if ((stars[star].getY() + STAR_RADIUS) < bottomBound_) {
            resetStar(stars[star]);
          }
        }
      }
    }

    // Update mini stars for effects
    if (numberOfMiniStarGroups) {
      for (int i = 0; i != MAXIMUM_MINI_STAR_GROUPS; ++i) {
        if (!miniStars_[i].isWaiting()) {
          miniStars_[i].update();

          // if slow enough, wait().
          if (miniStars_[i].getSpeed() <= 0.01f) {
            miniStars_[i].wait();
            --numberOfMiniStarGroups;
          }
        }
      }
    }

    // Update bad stars
    // Chance of a bad star appearing. Will need to increase in proportion to progression
    unsigned long seconds = (unsigned long)time(NULL);
    random = Mother(&seconds) * 100;
    if (random <= 0.8f) {
      // if in chance, find a waiting star and set it off.
      for(int badStar = 0; badStar != maximumNumberOfBadStars_; ++badStar) {
        if (badStars_[badStar].isWaiting()) {
          starfall(badStars_[badStar], distanceTravelled);
          break;
        }
      } // end for
    }

    if (numberOfBadStars_) {
      // update all stars that aren't waiting
      for(int star = 0; star != maximumNumberOfBadStars_; ++star) {
        if(!badStars_[star].isWaiting()) {
          badStars_[star].update();

          // collision detection
          vec4 difference = vec4(badStars_[star].getX() - balloon->getX(), badStars_[star].getY() - balloon->getY(), 0, 0);
          float proximitySquared = difference.lengthSquared();

          if (proximitySquared < ((BALLOON_RADIUS + STAR_RADIUS - 4) * (BALLOON_RADIUS + STAR_RADIUS - 4))) {
            --lives_;
            if (soundOn) {
              char name[2];
              name[0] = 2 + '0';
              name[1] = 0;
              sound_manager::play(vec4(0, 0, 0, 0), name);
            }
            resetStar(badStars_[star]);
          }

          // tell any stars out of bounds to wait.
          else if ((badStars_[star].getY() + STAR_RADIUS) < bottomBound_) {
            resetStar(badStars_[star]);
          }
        }
      }
    }
  }

  void draw(shader &shader, texture* tex, mat4 &modelToProjection) {
    for(int star = 0; star != MAXIMUM_STARS; ++star) {
      if (!stars[star].isWaiting()) {
        if (stars[star].isCollected()) {
          greenStarSprite_.draw(stars[star].getPosition(), true, stars[star].getAngle(), shader, tex, modelToProjection);
        }
        else {
          yellowStarSprite_.draw(stars[star].getPosition(), true, stars[star].getAngle(), shader, tex, modelToProjection);
        }
      }
    }

    if (numberOfBadStars_) {
      for(int star = 0; star != maximumNumberOfBadStars_; ++star) {
        if (!badStars_[star].isWaiting()) {
          redStarSprite_.draw(badStars_[star].getPosition(), true, badStars_[star].getAngle(), shader, tex, modelToProjection);
        }
      }
    }
  }

  void drawMiniStars(shader &shader, texture* tex, mat4 &modelToProjection) {
    if (numberOfMiniStarGroups) {
      for (int i = 0; i != MAXIMUM_MINI_STAR_GROUPS; ++i) {
        if (!miniStars_[i].isWaiting()) {
          miniStars_[i].draw(miniStarSprite_, shader, tex, modelToProjection);
        }
      }
    }
  }

  unsigned int getScore() {return score_;}
  unsigned int getLives() {return lives_;}
};

